/**
*   Author: 小 阮  
*       - Version 1.0.0
*       - 14/01/2018
*/
var Obsevrer = function() {
    function Obsevrer(value) {
        this.value = value;
        this.walk(value);
    }
    Obsevrer.observe = function(value, vm) {
        if (vm === void 0) {
            vm = null;
        }
        if (!value || typeof value !== "object") return;
        return new Obsevrer(value);
    };
    Obsevrer.prototype.walk = function(obj) {
        var _this = this;
        Object.keys(obj).forEach(function(key) {
            _this.observeProperty(obj, key, obj[key]);
        });
    };
    Obsevrer.prototype.observeProperty = function(obj, key, val) {
        var dep = new Dep();
        var childObj = Obsevrer.observe(val);
        Object.defineProperty(obj, key, {
            enumerable: true,
            configurable: false,
            get: function() {
                if (Dep.target) dep.depend();
                return val;
            },
            set: function(newVal) {
                if (val === newVal) return;
                val = newVal;
                childObj = Obsevrer.observe(newVal);
                dep.notify();
            }
        });
    };
    return Obsevrer;
}();

var _uid = 0;

var Dep = function() {
    function Dep() {
        this.id = _uid++;
        this.subs = [];
    }
    Dep.prototype.addSub = function(sub) {
        this.subs.push(sub);
    };
    Dep.prototype.removeSub = function(sub) {
        var idx = this.subs.indexOf(sub);
        if (idx !== -1) this.subs.splice(idx, 1);
    };
    Dep.prototype.depend = function() {
        Dep.target.addDep(this);
    };
    Dep.prototype.notify = function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    };
    return Dep;
}();

Dep.target = null;

var Compile = function() {
    function Compile(el, vm) {
        this.$vm = vm;
        this.$el = this.isElementNode(el) ? el : document.querySelector(el);
        this.$fragment = this.nodeFragment(this.$el);
        this.compileElement(this.$fragment);
        this.$el.appendChild(this.$fragment);
    }
    Compile.prototype.compileElement = function(el) {
        var _this = this;
        var childNodes = el.childNodes;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{(.*)\}/;
            if (_this.isElementNode(node)) _this.compile(node); else if (_this.isTextNode(node) && reg.test(text)) _this.compileText(node, RegExp.$1);
            if (node.childNodes && node.childNodes.length) _this.compileElement(node);
        });
    };
    Compile.prototype.compile = function(node) {
        var _this = this;
        var nodeAttrs = node.attributes;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            var attrName = attr.name;
            if (_this.isDirective(attrName)) {
                var exp = attr.value;
                var dir = attrName.substring(2);
                if (_this.isEventDirective(dir)) compileUtil.eventHandle(node, _this.$vm, exp, dir); else compileUtil[dir] && compileUtil[dir](node, _this.$vm, exp);
                node.removeAttribute(attrName);
            }
        });
    };
    Compile.prototype.nodeFragment = function(el) {
        var fragment = document.createDocumentFragment(), child;
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    };
    Compile.prototype.compileText = function(node, exp) {
        compileUtil.text(node, this.$vm, exp);
    };
    Compile.prototype.isElementNode = function(node) {
        return node.nodeType === 1;
    };
    Compile.prototype.isTextNode = function(node) {
        return node.nodeType === 3;
    };
    Compile.prototype.isDirective = function(attr) {
        return attr.indexOf("x-") === 0;
    };
    Compile.prototype.isEventDirective = function(dir) {
        return dir.indexOf("on") === 0;
    };
    return Compile;
}();

var compileUtil = {
    html: function(node, vm, exp) {
        this.bind(node, vm, exp, "html");
    },
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, "text");
    },
    class: function(node, vm, exp) {
        this.bind(node, vm, exp, "class");
    },
    model: function(node, vm, exp) {
        this.bind(node, vm, exp, "model");
        var self = this, val = this._getVMVal(vm, exp);
        node.addEventListener("input", function(e) {
            var newVal = e.target.value;
            if (val === newVal) return;
            self._setVMVal(vm, exp, newVal);
            val = newVal;
        });
    },
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + "Updater"];
        updaterFn && updaterFn(node, this._getVMVal(vm, exp));
        new Watcher(vm, exp, function(value, oldValue) {
            updaterFn && updaterFn(node, value, oldValue);
        });
    },
    eventHandle: function(node, vm, exp, dir) {
        var eventType = dir.split(":")[1], fn = vm.$options.methods && vm.$options.methods[exp];
        if (eventType && fn) node.addEventListener(eventType, fn.bind(vm), false);
    },
    _getVMVal: function(vm, exp) {
        var val = vm;
        exp = exp.split(".");
        exp.forEach(function(k) {
            val = val[k.trim()];
        });
        return val;
    },
    _setVMVal: function(vm, exp, newVal) {
        var val = vm;
        exp = exp.split(".");
        exp.forEach(function(k, i) {
            if (i < exp.length - 1) val = val[k]; else val[k] = newVal;
        });
    }
};

var updater = {
    htmlUpdater: function(node, value) {
        if (value === void 0) {
            value = "";
        }
        node.innerHTML = value;
    },
    textUpdater: function(node, value) {
        if (value === void 0) {
            value = "";
        }
        node.textContent = value;
    },
    classUpdater: function(node, value, oldValue) {
        var className = node.className.replace(oldValue, "").replace(/\s$/, "");
        node.className = className + (className && String(value) ? " " : "") + value;
    },
    modelUpdater: function(node, value) {
        if (value === void 0) {
            value = "";
        }
        node.value = value;
    }
};

var Watcher = function() {
    function Watcher(vm, expOrFn, callback) {
        this.vm = vm;
        this.expOrFn = expOrFn;
        this.callback = callback;
        this.depIds = {};
        expOrFn = expOrFn.trim();
        if (typeof expOrFn === "function") this.getter = expOrFn; else this.getter = this.parseGetter(expOrFn);
        this.value = this.get();
    }
    Watcher.prototype.update = function() {
        this.run();
    };
    Watcher.prototype.run = function() {
        var newVal = this.get(), oldValue = this.value;
        if (newVal !== oldValue) {
            this.value = newVal;
            this.callback.call(this.vm, newVal, oldValue);
        }
    };
    Watcher.prototype.addDep = function(dep) {
        if (!this.depIds.hasOwnProperty(dep.id)) {
            dep.addSub(this);
            this.depIds[dep.id] = dep;
        }
    };
    Watcher.prototype.get = function() {
        Dep.target = this;
        var value = this.getter.call(this.vm, this.vm);
        Dep.target = null;
        return value;
    };
    Watcher.prototype.parseGetter = function(exp) {
        if (/[^\w.$]/.test(exp)) return;
        var exps = exp.split(".");
        return function(obj) {
            for (var i = 0, len = exps.length; i < len; i++) {
                if (!obj) return;
                obj = obj[exps[i]];
            }
            return obj;
        };
    };
    return Watcher;
}();

var MVVM = function() {
    function MVVM($options) {
        if ($options === void 0) {
            $options = {};
        }
        var _this = this;
        this.$options = $options;
        this._data = $options.data;
        Object.keys(this._data).forEach(function(key) {
            _this._proxyData(key);
        });
        Obsevrer.observe(this._data, this);
        new Compile($options.el || document.body, this);
    }
    MVVM.prototype._proxyData = function(key, setter, getter) {
        if (setter === void 0) {
            setter = undefined;
        }
        if (getter === void 0) {
            getter = undefined;
        }
        var self = this;
        setter = setter || Object.defineProperty(this, key, {
            enumerable: true,
            configurable: false,
            get: function proxyGetter() {
                return self._data[key];
            },
            set: function proxySetter(newVal) {
                self._data[key] = newVal;
            }
        });
    };
    return MVVM;
}();